在軟體設計中,我們經常面對類別之間的依賴關係。想像你正在開發一個龐大的系統,當中某個小功能需要變更,卻發現一改動就要牽扯到許多不相關的部分。這種情況下你是不是會開始懷疑,設計是否有問題?
這就是設計原則出現的原因,特別是「依賴反轉原則 (Dependency Inversion Principle, DIP)」,它能幫助我們建立靈活的系統,避免高耦合的噩夢。
依賴反轉原則是一種解決依賴問題的設計原則,依賴反轉原則強調:高層模組不應依賴於低層模組,兩者都應依賴於抽象。這聽起來可能有點抽象,但換句話說,就是我們要將具體的細節抽象化,讓不同層級的模組之間解耦不直接依賴彼此的實作,進而減少維護成本與錯誤的機會。
依賴反轉原則主要包含兩個重點:
用最白話的方式來理解,就是「大腦不應該直接控制肌肉運動,而是應該透過神經傳導」。同樣道理,應用程式的高層邏輯不應該直接依賴底層細節,而是應該透過一層抽象來進行溝通。
我們先來看一個不遵守依賴反轉原則的例子。
想像你正在設計一個點餐系統,在原始設計裡你可能會這樣實作:
class CreditCard {
public:
void pay() {
// 信用卡付款邏輯
}
};
class Order {
CreditCard creditCard;
public:
void processOrder() {
creditCard.pay();
}
};
這個程式看起來沒什麼問題,但如果突然要新增其他付款方式,例如 PayPal 呢?每當系統需要支援新的付款方式時,你都得改 Order
class 類別,這樣的設計很容易隨著功能擴充而變得脆弱。
在上面的範例中,Order
類別直接依賴於 CreditCard
類別,這就是違反依賴反轉原則的設計。這樣一來,只要 CreditCard
類別有變動,或者你要加入其他付款方式,Order
的實作就需要大幅改動,這導致系統的維護變得更加困難,擴充性也大打折扣。
我們可以透過抽象的方式解決這個問題。這裡的解決方案就是將付款方式抽象出來,讓 Order
類別不再直接依賴於具體的 CreditCard
,而是依賴於一個抽象的付款介面:
class PaymentMethod {
public:
virtual void pay() = 0;
};
class CreditCard : public PaymentMethod {
public:
void pay() override {
// 信用卡付款邏輯
}
};
class PayPal : public PaymentMethod {
public:
void pay() override {
// PayPal付款邏輯
}
};
class Order {
PaymentMethod& paymentMethod;
public:
Order(PaymentMethod& method) : paymentMethod(method) {}
void processOrder() {
paymentMethod.pay();
}
};
在這個範例中,Order
類別不再依賴具體的 CreditCard
或 PayPal
,而是依賴於 PaymentMethod
這個抽象介面。這樣,如果將來要新增其他付款方式,我們只需要實作新的付款類別(實作 PaymentMethod
class),而不必動到 Order
類別。這樣的設計就遵循了依賴反轉原則,達到了靈活且可擴展的目的。
當我們遵循依賴反轉原則,會發現系統變得更加彈性,維護性也提高了。具體實作的變動不會影響高層邏輯,這意味著在不改動核心業務邏輯的情況下,我們可以輕鬆替換或擴充底層的具體實作。
例如在付款系統中,隨著業務發展,可能會有新的付款方式不斷加入。依賴反轉原則讓我們在不改動核心訂單處理的情況下,直接新增其他付款方式,保持系統的穩定性與靈活性。
但這樣做也有一點缺點,就是會讓設計稍微複雜,特別是對於一些小型專案來說,過度抽象反而會增加開發的負擔。但隨著系統成長,這種設計方式能帶來的好處將遠大於初期的設計成本。
依賴反轉原則在很多場景中都非常實用,特別是在大型系統或複雜應用中。例如在網路應用中,我們常常會有不同的資料存取方式,如 SQL、NoSQL 或是基於第三方的 API。透過依賴反轉原則,我們可以為高層邏輯設計一個統一的資料存取介面,而實際的資料庫選擇則可以在不同情況下自由切換,而不影響應用層邏輯。
這樣的設計原則也可以應用在許多場景,例如日誌系統、通知系統、甚至是硬體裝置的驅動層。每當你需要擴充或替換具體實作,而不想改動核心邏輯時,依賴反轉原則都是你的好幫手。
更多C++語言相關的文章,歡迎追蹤我的部落格。
https://shengyu7697.github.io/dependency-inversion-principle/